I started this project making an really smooth controlled pickup system, I achieved this by adding damping, jiggle effect, rotation and throwing.
Together it looks like this:

using Cinemachine;

  public class Pickup : MonoBehaviour
  {
      [Header("Pickup Settings")]
      [SerializeField] private float pickupRange = 5;
      [SerializeField] private float moveForce = 1;
      [SerializeField] private Transform middlePos;
      [SerializeField] private float rotationSpeed = 5;
  
      [Header("Pickup Info")]
      [SerializeField] private PlayerLook playerL;
      [SerializeField] private CinemachineVirtualCamera vCam;
      [SerializeField] private LayerMask pickupLayer;
      private PullObject PullObjScript;
      private LineRenderer line;
  
      [Header("Throw Settings")]
      [SerializeField] private float timer = 1;
      private bool throwIt = false;
      private bool letGo = false;
      public bool IsThrowing = false;
  
      [HideInInspector] public GameObject heldObject;
      private float currentMass = 1;
  
      private Vector3 turn;
      public bool RotateEnabled = false;
  
      private void Start()
      {
          line = middlePos.gameObject.GetComponent<LineRenderer>();
          PullObjScript = GetComponent<PullObject>();
      }
  
      void Update()
      {
          // move picked object to hold position and keep moving it towards
          if (heldObject != null && RotateEnabled == false)
          {
              line.positionCount = 0;
  
              if (Vector3.Distance(heldObject.transform.position, middlePos.position) > 0.0f)
              {
                  Vector3 moveDiretion = (middlePos.position - heldObject.transform.position);
                  heldObject.GetComponent<Rigidbody>().AddForce(moveDiretion * moveForce);
                  heldObject.GetComponent<Rigidbody>().AddForce(-transform.up * heldObject.GetComponent<Rigidbody>().mass * 35);
              }
  
              if (heldObject.GetComponent<Rigidbody>().mass < 3)
              {
                  if (Input.GetKeyDown(KeyCode.R))
                  {
                      RotateEnabled = true;
                      playerL.ChangeMovement();
                      heldObject.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezeRotation;
                  }
              }
          }
  
          // enable object rotation while holding it
          if (RotateEnabled)
          {
              Vector3 moveDiretion = (middlePos.position - heldObject.transform.position);
              heldObject.GetComponent<Rigidbody>().AddForce(moveDiretion * moveForce);
  
              RotateObject();
          }
  
          // pickup object when pressing e
          if (Input.GetKeyDown(KeyCode.E))
          {
              if (heldObject == null)
              {
                  RaycastHit hit;
                  if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hit, pickupRange, pickupLayer))
                      PickupUpObject(hit.transform.gameObject);
              }
              else if (throwIt == false)
              {
                  DropObject();
                  playerL.ChangeMovement();
              }
          }
  
          if (heldObject != null)
          {
              heldObject.GetComponentInChildren<PickupGlow>().On();
  
              line.positionCount = 2;
              line.SetPosition(0, middlePos.position);
              line.SetPosition(1, heldObject.transform.position);
  
              float _distanceBetweenObj = Vector3.Distance(middlePos.transform.position, heldObject.transform.position);
              if (_distanceBetweenObj > 1.4f)
                  DropObject();
              else
              {
                  if (heldObject.GetComponent<Rigidbody>().mass < 3)
                  {
                      // if mouse is being hold, fov goes up and you throw harder the longer you hold it
                      if (Input.GetKey(KeyCode.Mouse0) && PullObjScript.HasObj == false)
                      {
                          if (_distanceBetweenObj > 1.4f)
                          {
                              DropObject();
                              letGo = true;
                              throwIt = false;
                              vCam.m_Lens.FieldOfView = Mathf.MoveTowards(vCam.m_Lens.FieldOfView, 60, 10 * Time.maximumDeltaTime);
                          }
                          else
                          {
                              IsThrowing = true;
                              vCam.m_Lens.FieldOfView += 5 * Time.deltaTime;
                              timer -= 0.1f * Time.deltaTime;
                              heldObject.GetComponent<Rigidbody>().mass = timer;
                              throwIt = true;
                          }
                      }
                  }
              }
  
              if (Input.GetKeyDown(KeyCode.E))
                  RotateEnabled = false;
          }
          else if (heldObject == null)
              playerL.movementOn = true;      
  
          // throw the object and throw fov back to default
          if (letGo)
          {
              IsThrowing = false;
              vCam.m_Lens.FieldOfView = Mathf.MoveTowards(vCam.m_Lens.FieldOfView, 60, 10 * Time.maximumDeltaTime);
              if (vCam.m_Lens.FieldOfView == 60)
                  letGo = false;
          }
  
          // let go off object and throw it with the force it has
          if (throwIt)
          {
              if (letGo == false && Input.GetKeyUp(KeyCode.Mouse0))
              {
                  if (RotateEnabled)
                      playerL.ChangeMovement();
  
                  heldObject.GetComponentInChildren<PickupGlow>().Off();
                  heldObject.GetComponent<Rigidbody>().mass += currentMass * 1.5f;
                  line.positionCount = 0;
                  letGo = true;
                  RotateEnabled = false;
                  timer = 1;
                  ThrowObject();
                  heldObject = null;
                  throwIt = false;
                  vCam.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>().m_FrequencyGain = 1;
              }
          }
  
          // if your on full force for throwing the frequency of the camera shake spikes up
          if (timer <= 0.3f)
          {
              timer = 0.3f;
              vCam.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>().m_FrequencyGain = 10;
              vCam.m_Lens.FieldOfView = 86.6f;
          }
      }
  
      private void RotateObject()
      {
          if (Input.GetKeyUp(KeyCode.R))
          {
              RotateEnabled = false;
              playerL.ChangeMovement();
          }
  
          if (Input.GetKeyDown(KeyCode.E))
              RotateEnabled = false;
  
          // rotate object with mouse movement
          float xInput = Input.GetAxis("Mouse X");
          float yInput = Input.GetAxis("Mouse Y");
          turn.x += xInput * rotationSpeed;
          turn.y += yInput * rotationSpeed;
  
          Transform cameraTransform = Camera.main.transform;
  
  #pragma warning disable CS0618 // Type or member is obsolete
          heldObject.transform.RotateAround(cameraTransform.up, xInput * Time.deltaTime * rotationSpeed);
          heldObject.transform.RotateAround(cameraTransform.right, -yInput * Time.deltaTime * rotationSpeed);
  #pragma warning restore CS0618 // Type or member is obsolete
      }
  
      /// <summary>
      /// get information on picked object
      /// </summary>
      /// 
      private void PickupUpObject(GameObject pickObj)
      {
          if (pickObj.GetComponent<Rigidbody>())
          {
              pickObj.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.None;
              Rigidbody objRig = pickObj.GetComponent<Rigidbody>();
              objRig.useGravity = false;
              objRig.drag = 10;
              heldObject = pickObj;
          }
      }
  
      /// <summary>
      /// throw object with information from mouse hold
      /// </summary>
      private void ThrowObject()
      {
          heldObject.GetComponentInChildren<PickupGlow>().Off();
          Rigidbody heldRig = heldObject.GetComponent<Rigidbody>();
          heldObject.GetComponent<Rigidbody>().useGravity = true;
          heldRig.drag = 1;
  
          heldObject.transform.parent = null;
          heldObject.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.None;
          heldObject.GetComponent<Rigidbody>().AddForce(transform.forward * 1000);
          heldObject = null;
      }
  
      /// <summary>
      /// when holding the object press e and it drops normal on the ground
      /// </summary>
      private void DropObject()
      {
          heldObject.GetComponentInChildren<PickupGlow>().Off();
          line.positionCount = 0;
          Rigidbody heldRig = heldObject.GetComponent<Rigidbody>();
          heldObject.GetComponent<Rigidbody>().useGravity = true;
          heldRig.drag = 1;
  
          heldObject.transform.parent = null;
          heldObject.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.None;
          heldObject = null;
  
          if (RotateEnabled)
          {
              playerL.ChangeMovement();
              RotateEnabled = false;
          }
      }
  
      public void DropObj()
      {
          heldObject.GetComponentInChildren<PickupGlow>().Off();
          line.positionCount = 0;
          heldObject.GetComponent<Rigidbody>().useGravity = true;
  
          heldObject.transform.parent = null;
          heldObject.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.None;
          heldObject = null;
  
          if (RotateEnabled)
              playerL.ChangeMovement();
      }
  }
  



Besides the pickup system I created Puzzles/MiniGames designed by the artists, for the player to solve also using the pickup and throwing mechanic.


I created scripts for the buttons used in the minigames to connect to that minigame.

public class PlateDown : MonoBehaviour
  {
      private Vector3 upPos;
      private Vector3 downPos;
  
      public bool On = false;
  
      void Start()
      {
          upPos = transform.position;
          downPos = transform.position -= new Vector3(0, 0.1f, 0);
      }
  
      void Update()
      {
          if (On)
              transform.position = Vector3.Lerp(transform.position, downPos, 1 * Time.deltaTime);
          else if (On == false)
              transform.position = Vector3.Lerp(transform.position, upPos, 1 * Time.deltaTime);
      }
  
      private void OnTriggerEnter(Collider other)
      {
          if (other.gameObject.CompareTag("Player"))
              EffectManager.instance.ScreenShake(1.3f, 4f, .5f);
  
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              AudioManager.instance.Play("PressurePlate");
      }
  
      private void OnTriggerStay(Collider other)
      {
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              On = true;
      }
  
      private void OnTriggerExit(Collider other)
      {
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              On = false;
      }
  }
  
public class LiftDoorPlate : MonoBehaviour
  {
      public Transform Plate;
      public Transform BackPos;
      public Transform Pos;
  
      private bool On = false;
  
      void Update()
      {
          if (On)
              Plate.transform.position = Vector3.MoveTowards(Plate.transform.position, Pos.position, 1 * Time.deltaTime);
          else
              Plate.transform.position = Vector3.MoveTowards(Plate.transform.position, BackPos.position, 1 * Time.deltaTime);
      }
  
      private void OnTriggerStay(Collider other)
      {
          ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
  
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              On = true;
      }
  
      private void OnTriggerExit(Collider other)
      {
          ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
  
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              On = false;
      }
  }
Random Image
public class BridgePuzzle : MonoBehaviour
  {
      public Transform Bridge;
      public Transform GoTo;
      public Transform GoToBack;
      public bool ClonePuzzle = false;
  
      private bool activated = false;
  
      public AudioSource Ending;
  
      private void Update()
      {
          if (activated)
          {
              if (ClonePuzzle)
                  Bridge.transform.localPosition = Vector3.MoveTowards(Bridge.transform.localPosition, GoTo.transform.localPosition, 0.5f * Time.deltaTime);
              else
                  Bridge.transform.localPosition = Vector3.MoveTowards(Bridge.transform.localPosition, GoTo.transform.localPosition, 1 * Time.deltaTime);
          }
          else
          {
              if (ClonePuzzle == false)
                  Bridge.transform.localPosition = Vector3.MoveTowards(Bridge.transform.localPosition, GoToBack.transform.localPosition, 1 * Time.deltaTime);
          }
      }
  
      private void OnTriggerEnter(Collider other)
      {
          if (Ending != null)
          {
              if (other.gameObject.CompareTag("Clone"))
              {
                  if (ClonePuzzle)
                      Ending.enabled = true;
              }
          }
      }
  
      private void OnTriggerStay(Collider other)
      {
          ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
  
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              activated = true;
      }
  
      private void OnTriggerExit(Collider other)
      {
          ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
  
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              activated = false;
      }
  }
  
Random Image
using Cinemachine;

  public class BridgePuzzle : MonoBehaviour
  {
      public Transform Bridge;
      public Transform GoTo;
      public Transform GoToBack;
      public bool ClonePuzzle = false;
  
      private bool activated = false;
  
      public AudioSource Ending;
  
      private void Update()
      {
          if (activated)
          {
              if (ClonePuzzle)
                  Bridge.transform.localPosition = Vector3.MoveTowards(Bridge.transform.localPosition, GoTo.transform.localPosition, 0.5f * Time.deltaTime);
              else
                  Bridge.transform.localPosition = Vector3.MoveTowards(Bridge.transform.localPosition, GoTo.transform.localPosition, 1 * Time.deltaTime);
          }
          else
          {
              if (ClonePuzzle == false)
                  Bridge.transform.localPosition = Vector3.MoveTowards(Bridge.transform.localPosition, GoToBack.transform.localPosition, 1 * Time.deltaTime);
          }
      }
  
      private void OnTriggerEnter(Collider other)
      {
          if (Ending != null)
          {
              if (other.gameObject.CompareTag("Clone"))
              {
                  if (ClonePuzzle)
                      Ending.enabled = true;
              }
          }
      }
  
      private void OnTriggerStay(Collider other)
      {
          ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
  
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              activated = true;
      }
  
      private void OnTriggerExit(Collider other)
      {
          ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
  
          if (other.gameObject.CompareTag("Player") || other.gameObject.CompareTag("Clone") || other.gameObject.CompareTag("CubeNormal"))
              activated = false;
      }
  }
Random Image



For easier gameplay I created the checkpoint system. By entering a level you enter a collider that lets you respawn at the beginning of the level also resetting every object in the level. Also added a fade-in so it's smoother

public class CheckPointManager : MonoBehaviour
  {
      public CheckPointItems[] CheckPoints;
  
      public Transform spawnPoint;
  
      [SerializeField] private CharacterController characterCont;
      private UnityEngine.CharacterController characterController;
  
      [SerializeField] private PlayerLook playerL;
      [SerializeField] private GameObject player;
      [SerializeField] private Animator fade;
      [SerializeField] private Transform cam;
      [SerializeField] private Transform oldCam;
      [SerializeField] private DialogueSystem dialogue;
  
      private bool kill = false;
  
      private void Start()
      {
          cam.transform.localRotation = Quaternion.Euler(90, transform.localRotation.y, transform.localRotation.z);
          characterController = FindObjectOfType<UnityEngine.CharacterController>();
      }
  
      private void Update()
      {
          if (kill)
              cam.position = Vector3.Lerp(cam.position, new Vector3(cam.position.x, cam.position.y + 5, cam.position.z), 0.3f * Time.deltaTime);
  
          if (Input.GetKeyDown(KeyCode.L))
          {
              Respawn();
          }
      }
  
      public void Respawn()
      {
          StartCoroutine(RespawnAtCheckPoint(oldCam));
      }
  
      public void AddCheckPoint(Transform _checkPointPos)
      {
          spawnPoint = _checkPointPos;
      }
  
      private IEnumerator RespawnAtCheckPoint(Transform _OldPos)
      {
          kill = true;
          playerL.enabled = false;
          cam.transform.localRotation = Quaternion.Euler(0, transform.localRotation.y, transform.localRotation.z);
          dialogue.PlayRandomDialogue();
          characterCont.enabled = false;
          characterController.enabled = false;
          fade.Play("Eyes");
          yield return new WaitForSeconds(2);
          kill = false;
          cam.transform.localRotation = Quaternion.Euler(0, 0, 0);
          player.transform.position = spawnPoint.position;
          cam.position = _OldPos.position;
          ResetRoom();
          yield return new WaitForSeconds(1);
          playerL.enabled = true;
          characterCont.enabled = true;
          characterController.enabled = true;
      }
  
      private void ResetRoom()
      {
          FindObjectOfType<Controller>().DestroyClone();
          int _roomCode = -1;
  
          for (int i = 0; i < CheckPoints.Length; i++)
          {
              if (spawnPoint == CheckPoints[i].CheckPoint.spawnPos)
                  _roomCode = i;
          }
  
          if (CheckPoints[_roomCode].ObjectsToReset.Length > 0 && _roomCode >= 0)
          {
              foreach (GameObject roomItem in CheckPoints[_roomCode].ObjectsToReset)
              {
                  if (roomItem.TryGetComponent(out ObjectRespawn objectRespawn))
                  {
                      objectRespawn.ResetObject();
                  }
                  else if (roomItem.GetComponentInChildren<PlateDown>())
                  {
                      roomItem.GetComponentInChildren<PlateDown>().On = false;
                  }
              }
          }
      }
  }
  
  [System.Serializable]
  public class CheckPointItems
  {
      public CheckPoint CheckPoint;
  
      public GameObject[] ObjectsToReset;
  }
  
public class Kill : MonoBehaviour
  {
      private CheckPointManager checkPointManager;
      private Controller cont;
  
      void Start()
      {
          cont = FindObjectOfType<Controller>();
          checkPointManager = FindObjectOfType();    
      }
  
      private void OnTriggerEnter(Collider other)
      {
          if (other.gameObject.CompareTag("Player"))
              checkPointManager.Respawn();
  
          if (other.gameObject.CompareTag("Clone"))
              cont.DestroyClone();
      }
  }



As extra I created a transition between levels using an elevator

using Cinemachine;

  public class Elevator : MonoBehaviour
  {
      public GameObject Player;
      public Transform ElevatorPos;
  
      private UnityEngine.CharacterController characterCont;
  
      private Animator elevatorAnim;
  
      CinemachineVirtualCamera vCam;
  
      private void Start()
      {
          vCam = FindObjectOfType<PlayerRecorder>().gameObject.GetComponentInChildren<CinemachineVirtualCamera>();
          characterCont = FindObjectOfType<UnityEngine.CharacterController>();
          elevatorAnim = GetComponentInChildren<Animator>();
      }
  
      public IEnumerator ElevatorChange()
      {
          AudioManager.instance.Play("ElevatorOpen");
          elevatorAnim.Play("ElevatorClose");
          yield return new WaitForSeconds(2);
          AudioManager.instance.Play("ElevatorMusic");
          vCam.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>().m_FrequencyGain = 8;
          characterCont.enabled = false;
          Player.transform.SetParent(this.transform);
          yield return new WaitForSeconds(0.01f);
          if (ElevatorPos != null)
          {
              transform.position = ElevatorPos.position;
              transform.rotation = ElevatorPos.rotation;
          }
          yield return new WaitForSeconds(0.01f);
          Player.transform.SetParent(null);
          characterCont.enabled = true;
          yield return new WaitForSeconds(10);
          vCam.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>().m_FrequencyGain = 1;
          yield return new WaitForSeconds(1);
          AudioManager.instance.Play("ElevatorEnd");
          elevatorAnim.Play("ElevatorOpen");
      }
  }



And ofcourse I added cutscenes to the game for getting more feeling into it.



And The last thing I worked on is the main menu. Having a background music, working button and a moving background





Date: Jun 27, 2022

– Project: Shortburn

– Duration: 6 Week

– Team: 2 Devs, 3 Artists

My Part: Pickup, PlayerRespawn, Cutscenes, Animations, Puzzles, UI, Object Reset, VideoPlayers, Pause, CheckpointSystem, ElevatorTransition, PressurePlates

Summary:

In project Short Burn, me and a small group tried to achieve the highest possible quality in an indie First Person Puzzler. Our core mechanic is to record your movement(Movement, Rotation) and spawn a clone that does that exact movement. we made this game with the following game design patterns: Flow, Pacing, Cells, Portals, Pick ups, Lay-out